/*
 * Copyright (c) 2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.protwarrior;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;

import yawning.mew.ProcEffect;
import yawning.mew.Procs;
import yawning.mew.BuffsDebuffs.Debuff;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.character.CharacterModel.AttackResult;
import yawning.mew.protwarrior.ProtWarriorSimulation.Action;
import yawning.mew.protwarrior.ProtWarriorSimulation.SimulatorProperty;
import yawning.mew.sim.SimulationAction;
import yawning.mew.sim.SimulationEngine;
import yawning.mew.sim.SimulationLog;
import yawning.mew.sim.SimulationProc;
import yawning.mew.sim.SimulationProcStatus;
import yawning.mew.sim.SimulationTarget;
import yawning.mew.sim.SimulationEventTargetMagic.TargetMagicCallback;
import yawning.mew.sim.SimulationEventTargetSwing.TargetSwingCallback;
import yawning.mew.sim.SimulationEventTargetYellow.TargetYellowCallback;
import yawning.mew.sim.SimulationManager.Property;
import yawning.mew.sim.SimulationProc.EffectCallback;
import yawning.mew.sim.Strategy.IStatus;

public class ProtWarriorSimulator implements IStatus {
	SimulationEngine mSimEngine;
	ProtWarriorModel mModel;

	boolean mIsLogging = false;
	SimulationLog mLog;

	SimulationTarget mActiveTarget;
	ProtWarriorTargetData mActiveTargetData;
	List<SimulationTarget> mTargetList;

	boolean mUnlimitedRage = false;

	boolean mHavePotion;
	boolean mPrePotion;

	double mRage;
	long mRageSpent;
	long mRageGainedCountThreat;
	double mMobMeleeRageGained;
	double mMobYellowRageGained;
	double mMobMagicRageGained;
	double mShieldSpecializationRageGained;
	double mRageOverflow;

	long mMobWhiteBaseDamage;
	long mMobWhiteDamageTaken;
	long mMobWhiteDamageBlocked;
	int mNrMobWhiteMiss;
	int mNrMobWhiteDodge;
	int mNrMobWhiteParry;
	int mNrMobWhiteBlock;
	int mNrMobWhiteCritBlock;
	int mNrMobWhiteCrit;
	int mNrMobWhiteHit;

	long mMobYellowBaseDamage;
	long mMobYellowDamageTaken;
	long mMobYellowDamageBlocked;
	int mNrMobYellowMiss;
	int mNrMobYellowDodge;
	int mNrMobYellowParry;
	int mNrMobYellowBlock;
	int mNrMobYellowCritBlock;
	int mNrMobYellowHit;

	long mMobMagicBaseDamage;
	long mMobMagicDamageTaken;
	int mNrMobMagicMiss;
	int mNrMobMagicHit;

	long mProcPhysDamage;
	long mProcSpellDamage;
	long mProcHeal;

	// Stash damage taken for giggles.
	List<Double> mMobWhiteDamageTakenSamples;
	List<Double> mMobYellowDamageTakenSamples;
	List<Double> mMobMagicDamageTakenSamples;

	BastionOfDefenseEvent mBastionOfDefenseEv;
	BattleTranceEvent mBattleTranceEv;
	BloodCrazeEvent mBloodCrazeEv;
	DeepWoundsEvent mDeepWoundsEv;
	HeroismEvent mHeroismEv;
	HoldTheLineEvent mHoldTheLineEv;
	ImpendingVictoryEvent mImpendingVictoryEv;
	MeleeEvent mMeleeEv;
	RudeInterruptionEvent mRudeInterruptionEv;
	SwordAndBoardEvent mSwordAndBoardEv;
	ThunderstruckEvent mThunderstruckEv;
	Tier_12_2pcEvent mTier_12_2pcEv;
	Tier_12_Dps_2pcEvent mTier_12_Dps_2pcEv;
	Tier_13_2pcEvent mTier_13_2pcEv;
	ToTTEvent mToTTEv;
	UnholyFrenzyEvent mUnholyFrenzyEv;
	VengeanceEvent mVengeanceEv;

	BattleShoutAction mBattleShoutAction;
	BerserkerRageAction mBerserkerRageAction;
	ChargeAction mChargeAction;
	CleaveAction mCleaveAction;
	CommandingShoutAction mCommandingShoutAction;
	ConcussionBlowAction mConcussionBlowAction;
	DemoralizingShoutAction mDemoralizingShoutAction;
	EnragedRegenerationAction mEnragedRegenerationAction;
	HeroicStrikeAction mHeroicStrikeAction;
	HeroicThrowAction mHeroicThrowAction;
	InnerRageAction mInnerRageAction;
	LastStandAction mLastStandAction;
	PotionAction mPotionAction;
	PummelAction mPummelAction;
	RecklessnessAction mRecklessnessAction;
	RendAction mRendAction;
	RevengeAction mRevengeAction;
	RetaliationAction mRetaliationAction;
	ShieldBlockAction mShieldBlockAction;
	ShieldSlamAction mShieldSlamAction;
	ShieldWallAction mShieldWallAction;
	ShockwaveAction mShockwaveAction;
	ThunderClapAction mThunderClapAction;
	VictoryRushAction mVictoryRushAction;

	double mHeroicStrikeCooldownExpireTime;		// Heroic Strike/Cleave linked CD.
	double mShatteringThrowCooldownExpireTime;
	double mShoutCooldownExpireTime;

	class ProtWarriorTargetData extends SimulationTarget.TargetData {
		ProtWarriorTargetData(SimulationTarget t) {
			t.super(mSimEngine);

			mDevastateAction = new DevastateAction(ProtWarriorSimulator.this, this);
			mShatteringThrowEv = new ShatteringThrowEvent(ProtWarriorSimulator.this, this);
		}

		DevastateAction mDevastateAction;
		ShatteringThrowEvent mShatteringThrowEv;
	}

	ProtWarriorSimulator(SimulationEngine su) {
		mSimEngine = su;
		mModel = new ProtWarriorModel(mSimEngine.getToon(), mSimEngine.getConfig());
		mModel.setSimulationEngine(mSimEngine);

		mActiveTarget = mSimEngine.getPrimaryTarget();
		mActiveTarget.setTargetData(new ProtWarriorTargetData(mActiveTarget));
		mActiveTargetData = (ProtWarriorTargetData) mActiveTarget.getTargetData();
		mModel.setTarget(mActiveTarget.getTarget());
		mTargetList = new ArrayList<SimulationTarget>();
		mTargetList.add(mActiveTarget);

		mIsLogging = mSimEngine.isLogging();
		mLog = mSimEngine.getLog();

		mUnlimitedRage = mSimEngine.getConfig().getBool(SimulatorProperty.UNLIMITEDRAGE);
		mRage = mUnlimitedRage ? 100 : 0;
		if (!mModel.mBuffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW)) mShatteringThrowCooldownExpireTime = Double.MAX_VALUE;

		mMobWhiteDamageTakenSamples = mActiveTarget.getTargetSwingEv().isUp() ? new ArrayList<Double>((int) Math.ceil(mSimEngine.getEndTime() / mActiveTarget.getTarget().getSwingInterval())) : null;
		mMobYellowDamageTakenSamples = mActiveTarget.getTargetYellowEv().isUp() ? new ArrayList<Double>((int) Math.ceil(mSimEngine.getEndTime() / mActiveTarget.getTarget().getYellowInterval())) : null;
		mMobMagicDamageTakenSamples = mActiveTarget.getTargetMagicEv().isUp() ? new ArrayList<Double>((int) Math.ceil(mSimEngine.getEndTime() / mActiveTarget.getTarget().getMagicInterval())) : null;

		// Setup the base events
		mBastionOfDefenseEv = new BastionOfDefenseEvent(this);
		mBattleTranceEv = new BattleTranceEvent(this);
		mBloodCrazeEv = new BloodCrazeEvent(this);
		mDeepWoundsEv = new DeepWoundsEvent(this);
		mHeroismEv = new HeroismEvent(this);
		mHoldTheLineEv = new HoldTheLineEvent(this);
		mImpendingVictoryEv = new ImpendingVictoryEvent(this);
		mMeleeEv = new MeleeEvent(this);
		mRudeInterruptionEv = new RudeInterruptionEvent(this);
		mSwordAndBoardEv = new SwordAndBoardEvent(this);
		mThunderstruckEv = new ThunderstruckEvent(this);
		mTier_12_2pcEv = new Tier_12_2pcEvent(this);
		mTier_12_Dps_2pcEv = new Tier_12_Dps_2pcEvent(this);
		mTier_13_2pcEv = new Tier_13_2pcEvent(this);
		mToTTEv = new ToTTEvent(this);
		mUnholyFrenzyEv = new UnholyFrenzyEvent(this);
		mVengeanceEv = new VengeanceEvent(this);

		// Setup the base actions
		mBattleShoutAction = new BattleShoutAction(this);
		mBerserkerRageAction = new BerserkerRageAction(this);
		mChargeAction = new ChargeAction(this);
		mCleaveAction = new CleaveAction(this);
		mCommandingShoutAction = new CommandingShoutAction(this);
		mConcussionBlowAction = new ConcussionBlowAction(this);
		mDemoralizingShoutAction = new DemoralizingShoutAction(this);
		mEnragedRegenerationAction = new EnragedRegenerationAction(this);
		mHeroicStrikeAction = new HeroicStrikeAction(this);
		mHeroicThrowAction = new HeroicThrowAction(this);
		mInnerRageAction = new InnerRageAction(this);
		mLastStandAction = new LastStandAction(this);
		mPotionAction = new PotionAction(this);
		mPummelAction = new PummelAction(this);
		mRecklessnessAction = new RecklessnessAction(this);
		mRendAction = new RendAction(this);
		mRevengeAction = new RevengeAction(this);
		mRetaliationAction = new RetaliationAction(this);
		mShieldBlockAction = new ShieldBlockAction(this);
		mShieldSlamAction = new ShieldSlamAction(this);
		mShieldWallAction = new ShieldWallAction(this);
		mShockwaveAction = new ShockwaveAction(this);
		mThunderClapAction = new ThunderClapAction(this);
		mVictoryRushAction = new VictoryRushAction(this);

		// Setup incoming damage handlers.
		mActiveTarget.getTargetSwingEv().setCharacterModel(mModel);
		mActiveTarget.getTargetSwingEv().addTargetSwingCallback(new TargetSwingCallback() {
			@Override
			public void onTargetSwing(AttackResult result, int baseDamage, int damage) {
				//
				// Calculate rage gain.
				//
				// Per EJ:
				//  UD = Unmitigated Damage Taken, a = constant, and H = Maximum Health.
				//  Gain >= 2:
				//   R = (UD*a)/H = Rage
				//   a = ???????
				//  Gain < 2:
				//   R = 1 + (UD*b)/H
				//   b = ???????
				//
				// Notes:
				//  * FIXME: Barkskin/SI affect Rage, so I would assume Shield Wall does too (How?).  Armor does not.
				//  * FIXME: My minion was lazy and only got me an approximate value for A) for gain >=2 yellow damage.
				//            For now use that for "a", and use the bear yellow value for b.
				//
				// Source:
				//  * http://elitistjerks.com/f73/t105117-cataclysm_bear_theorycraft/#post1767812
				//

				double baseRageDamage = (double) baseDamage;

				double a = 21.25;
				double b = 9.77;
				double rageGained = baseRageDamage * a / mModel.getHealth();
				if (rageGained < 2)
					rageGained = 1.0f + baseRageDamage * b / mModel.getHealth();

				if (mBerserkerRageAction.isUp()) rageGained *= 2;

				mMobMeleeRageGained += rageGained;
				onRageGain(rageGained);
				if (result == AttackResult.MISS) {
					mNrMobWhiteMiss++;

					if (mIsLogging) mLog.log("Target White Attack: MISS\n");
					mVengeanceEv.onAttacked(0);
					mMobWhiteDamageTakenSamples.add(0.0);
				} else if (result == AttackResult.DODGE) {
					mNrMobWhiteDodge++;

					if (mIsLogging) mLog.log("Target White Attack: DODGE\n");

					if (mBastionOfDefenseEv.mIsEnabled) mBastionOfDefenseEv.onTrigger();
					mVengeanceEv.onAttacked(0);
					mRevengeAction.onTrigger();
					mMobWhiteDamageTakenSamples.add(0.0);
				} else if (result == AttackResult.PARRY) {
					mNrMobWhiteParry++;

					if (mIsLogging) mLog.log("Target White Attack: PARRY\n");

					mHoldTheLineEv.onParry();
					if (mBastionOfDefenseEv.mIsEnabled) mBastionOfDefenseEv.onTrigger();
					mVengeanceEv.onAttacked(0);
					mRevengeAction.onTrigger();
					mMobWhiteDamageTakenSamples.add(0.0);
				} else if (result == AttackResult.BLOCK) {
					double blockedDamage = damage - (damage * mModel.multDRBlock);
					if (mSimEngine.getRandom().nextBoolean(mModel.getCriticalBlockChance(mHoldTheLineEv.isUp()))) {
						// Critical block.
						blockedDamage = blockedDamage * 2;
						mNrMobWhiteCritBlock++;
						damage -= blockedDamage;

						// Tier 13 (Comes after Block?)
						double preTier13Damage = damage;
						damage = mTier_13_2pcEv.onDamage(damage);

						if (mIsLogging) mLog.log("Target White Attack: BLOCK (Crit) Damage: %d Blocked: %d Absorbed: %d\n", Math.round(damage), Math.round(blockedDamage), Math.round(preTier13Damage - damage));
					} else {
						mNrMobWhiteBlock++;
						damage -= blockedDamage;

						// Tier 13 (Comes after Block?)
						double preTier13Damage = damage;
						damage = mTier_13_2pcEv.onDamage(damage);

						if (mIsLogging) mLog.log("Target White Attack: BLOCK Damage: %d Blocked: %d Absorbed: %d\n", Math.round(damage), Math.round(blockedDamage), Math.round(preTier13Damage - damage));
					}

					mMobWhiteDamageBlocked += blockedDamage;
					mMobWhiteDamageTaken += damage;

					onRageGainThreat(mModel.shieldSpecRage);
					mShieldSpecializationRageGained += mModel.shieldSpecRage;
					if (mBastionOfDefenseEv.mIsEnabled) mBastionOfDefenseEv.onTrigger();
					if (mBloodCrazeEv.mIsEnabled) mBloodCrazeEv.onTrigger();
					if (mRetaliationAction.isUp()) mRetaliationAction.onTrigger();
					mVengeanceEv.onAttacked(damage);
					mRevengeAction.onTrigger();
					mMobWhiteDamageTakenSamples.add((double) damage);
				} else {
					// Hit/Crit handling is mostly unified.
					double preTier13Damage = damage;
					damage = mTier_13_2pcEv.onDamage(damage);
					if (result == AttackResult.CRIT) mNrMobWhiteCrit++; else mNrMobWhiteHit++;
					if (mIsLogging) mLog.log("Target White Attack: %s Damage: %d Absorbed: %d\n", result, Math.round(damage), Math.round(preTier13Damage - damage));

					mMobWhiteDamageTaken += damage;

					if (mBloodCrazeEv.mIsEnabled) mBloodCrazeEv.onTrigger();
					if (mRetaliationAction.isUp()) mRetaliationAction.onTrigger();
					mVengeanceEv.onAttacked(damage);
					mMobWhiteDamageTakenSamples.add((double) damage);
				}

				mMobWhiteBaseDamage += baseDamage;
			}
		});

		mActiveTarget.getTargetYellowEv().setCharacterModel(mModel);
		mActiveTarget.getTargetYellowEv().addTargetYellowCallback(new TargetYellowCallback() {
			final boolean mIsBlockable = mActiveTarget.getTargetYellowEv().mBlockable;

			@Override
			public void onTargetYellow(AttackResult result, int baseDamage, int damage) {
				//
				// Calculate rage gain.
				//
				// XXX: This is supposedly different for yellow attacks.  But since I have no way
				// of working out the formula, use the white attack one for now.
				//
				// Per EJ:
				//  UD = Unmitigated Damage Taken, a = constant, and H = Maximum Health.
				//  Gain >= 2:
				//   R = (UD*a)/HR = Rage
				//   a = 20.1 w/o NR, 18.92 w NR
				//  Gain < 2:
				//   R = 1 + (UD*b)/H
				//   b = 9.77 w NR (No idea about without, but everyone should have it so who cares?)
				//
				// Notes:
				//  * FIXME: Barkskin/SI affect Rage, so I would assume Shield Wall does too (How?).  Armor does not.
				//  * FIXME: My minion was lazy and only got me an approximate value for A) for gain >=2 yellow damage.
				//            For now use that for "a", and use the bear yellow value for b.
				//

				double baseRageDamage = (double) baseDamage;
				double a = 21.25;
				double b = 9.77;

				double rageGained = baseRageDamage * a / mModel.getHealth();
				if (rageGained < 2)
					rageGained = 1.0f + baseRageDamage * b / mModel.getHealth();

				if (mBerserkerRageAction.isUp()) rageGained *= 2;

				mMobYellowRageGained += rageGained;
				onRageGain(rageGained);
				if (result == AttackResult.MISS) {
					mNrMobYellowMiss++;

					if (mIsLogging) mLog.log("Target Yellow Attack: MISS\n");

					mVengeanceEv.onAttacked(0);
					mMobYellowDamageTakenSamples.add(0.0);
				} else if (result == AttackResult.DODGE) {
					mNrMobYellowDodge++;

					if (mIsLogging) mLog.log("Target Yellow Attack: DODGE\n");

					if (mBastionOfDefenseEv.mIsEnabled) mBastionOfDefenseEv.onTrigger();
					mVengeanceEv.onAttacked(0);
					mRevengeAction.onTrigger();
					mMobYellowDamageTakenSamples.add(0.0);
				} else if (result == AttackResult.PARRY) {
					mNrMobYellowParry++;

					if (mIsLogging) mLog.log("Target Yellow Attack: PARRY\n");

					mHoldTheLineEv.onParry();
					if (mBastionOfDefenseEv.mIsEnabled) mBastionOfDefenseEv.onTrigger();
					mVengeanceEv.onAttacked(0);
					mRevengeAction.onTrigger();
					mMobYellowDamageTakenSamples.add(0.0);
				} else if (result == AttackResult.BLOCK && mIsBlockable) {
					double blockedDamage = damage - (damage * mModel.multDRBlock);
					if (mSimEngine.getRandom().nextBoolean(mModel.getCriticalBlockChance(mHoldTheLineEv.isUp()))) {
						// Critical block.
						blockedDamage = blockedDamage * 2;
						mNrMobYellowCritBlock++;
						damage -= blockedDamage;

						// Tier 13 (Comes after Block?)
						double preTier13Damage = damage;
						damage = mTier_13_2pcEv.onDamage(damage);

						if (mIsLogging) mLog.log("Target Yellow Attack: BLOCK (Crit) Damage: %d Blocked: %d\n", Math.round(damage), Math.round(blockedDamage), Math.round(preTier13Damage - damage));
					} else {
						mNrMobYellowBlock++;
						damage -= blockedDamage;

						// Tier 13 (Comes after Block?)
						double preTier13Damage = damage;
						damage = mTier_13_2pcEv.onDamage(damage);

						if (mIsLogging) mLog.log("Target Yellow Attack: BLOCK Damage: %d Blocked: %d\n", Math.round(damage), Math.round(blockedDamage), Math.round(preTier13Damage - damage));
					}

					mMobYellowDamageBlocked += blockedDamage;
					mMobYellowDamageTaken += damage;

					onRageGainThreat(mModel.shieldSpecRage);
					mShieldSpecializationRageGained += mModel.shieldSpecRage;
					if (mBastionOfDefenseEv.mIsEnabled) mBastionOfDefenseEv.onTrigger();
					if (mBloodCrazeEv.mIsEnabled) mBloodCrazeEv.onTrigger();
					if (mRetaliationAction.isUp()) mRetaliationAction.onTrigger();
					mVengeanceEv.onAttacked(damage);
					mRevengeAction.onTrigger();
					mMobYellowDamageTakenSamples.add((double) damage);
				} else {
					double preTier13Damage = damage;
					damage = mTier_13_2pcEv.onDamage(damage);
					mNrMobYellowHit++;
					if (mIsLogging) mLog.log("Target Yellow Attack: HIT Damage: %d Absorbed: %d\n", Math.round(damage), Math.round(preTier13Damage - damage));
					mMobYellowDamageTaken += damage;

					if (mBloodCrazeEv.mIsEnabled) mBloodCrazeEv.onTrigger();
					if (mRetaliationAction.isUp()) mRetaliationAction.onTrigger();
					mVengeanceEv.onAttacked(damage);
					mMobYellowDamageTakenSamples.add((double) damage);
				}

				mMobYellowBaseDamage += baseDamage;
			}
		});

		mActiveTarget.getTargetMagicEv().setCharacterModel(mModel);
		mActiveTarget.getTargetMagicEv().addTargetMagicCallback(new TargetMagicCallback() {
			@Override
			public void onTargetMagic(AttackResult result, int baseDamage, int damage) {
				//
				// Calculate rage gain.
				//
				// Per EJ:
				//  R = Rage gained, D = Damage taken, a = 40.5, and H = Maximum Health
				//  Gain >= 2:
				//   R = D*a/H
				//  Gain < 2:
				//   R = 1 + D*a1/H where a1 = 20.25
				//
				// Notes:
				//  * FIXME: Get values for a warrior instead of a Feral Druid.
				//
				double a = 40.5;
				double b = 20.25;
				double rageGained = (double) damage * a / mModel.getHealth();
				if (rageGained < 2)
					rageGained = 1.0f + (double) damage * b / mModel.getHealth();

				if (mBerserkerRageAction.isUp()) rageGained *= 2;

				mMobMagicRageGained += rageGained;
				onRageGain(rageGained);
				if (result == AttackResult.MISS) {
					mNrMobMagicMiss++;

					if (mIsLogging) mLog.log("Target Magic Attack: MISS\n");
					mVengeanceEv.onAttacked(0);
					mMobMagicDamageTakenSamples.add(0.0);
				} else {
					mNrMobMagicHit++;
					if (mIsLogging) mLog.log("Target Magic Attack: HIT Damage %d\n", Math.round(damage));
					mVengeanceEv.onAttacked(damage);
					mMobMagicDamageTaken += damage;
					mMobMagicDamageTakenSamples.add((double) damage);
				}

				mMobMagicBaseDamage += baseDamage;
			}
		});

		// Proc support.

		// Install handlers for the special procs.
		mSimEngine.registerProcEffectCallback(Effect.PHYSICAL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(SimulationTarget target, int n) {
				if (n <= 0) return;

				AttackResult result = mModel.getYellowResult();
				double damage = n;

				if (result != AttackResult.MISS && result != AttackResult.DODGE) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multCrit;
					}

					damage *= getDirectDamageMult();
					mProcPhysDamage += Math.round(damage);

					if (mIsLogging) mLog.logAppend(" %s Damage: %d", result, Math.round(damage));
				} else {
					if (mIsLogging) mLog.logAppend(" %s", result);
				}
			}
		});
		mSimEngine.registerProcEffectCallback(Effect.SPELL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(SimulationTarget target, int n) {
				if (n <= 0) return;

				AttackResult result = mModel.getSpellResult();
				double damage = n;

				if (result != AttackResult.MISS) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multSpellCrit;
					}

					damage *= getSpellDamageMult();

					mProcSpellDamage += Math.round(damage);

					if (mIsLogging) mLog.logAppend(" %s Damage: %d", result, Math.round(damage));
				} else {
					if (mIsLogging) mLog.logAppend(" MISS");
				}
			}
		});
		mSimEngine.registerProcEffectCallback(Effect.SPELL_ID, new EffectCallback() {
			@Override
			public void onEffect(SimulationTarget target, int n) {
				// DMC:Hurricane
				// As of 4.0.6, use the melee crit multipler/hit table.
				if (n == 62051) {
					// Just use the stealth yellow attack table resolution since it ignores Dodge/Glance.
					AttackResult result = mModel.getYellowStealthResult(0);
					double damage = 7000;

					if (result != AttackResult.MISS) {
						// As of 4.2 PTR, this is no longer capable of critting.
//						if (result == AttackResult.CRIT) {
//							damage *= mModel.multCrit;
//						}

						damage *= getSpellDamageMult();

						if (mIsLogging) mLog.logAppend(" Damage: %d", Math.round(damage));

						mProcSpellDamage += Math.round(damage);
					}
				}

				// Souldrinker
				// Notes: 2012/12/15 - Testing by Demo w/Normal (Item ID 77193)
				//  * @177887 hp (No buffs/debuffs) - 2668 hit 5656 heal
				//  * (CoE) 2882 hit 6109 heal
				//  * Can not crit.
				if (n == 78488 || n == 77193 || n == 78479) {
					double coeff = 0.0;
					switch (n) {
					case 78488: coeff = 0.013; break;
					case 77193: coeff = 0.015; break;
					case 78479: coeff = 0.017; break;
					}

					double damage = mModel.getHealth() * coeff;
					damage *= getSpellDamageMult();
					double heal = damage * 2 * mModel.multHeal;		// The proc heal gets a 1.06 multiplier from Field Dressing.

					if (mIsLogging) mLog.logAppend(" Damage: %d Heal: %d", Math.round(damage), Math.round(heal));

					mProcSpellDamage += Math.round(damage);
					mProcHeal += Math.round(heal);
				}

				// Troll Berserking (Racial)
				if (n == 26297) {
					mModel.multiplyMeleeHaste(1.2);
					mModel.multiplySpellHaste(1.2);
				} else if (n == -26297) {
					mModel.multiplySpellHaste(1.0 / 1.2);
					mModel.multiplyMeleeHaste(1.0 / 1.2);
				}
			}
		});

		for (ProcEffect effect : Procs.getInstance().getProcsByProperty(mSimEngine.getConfig().getProperties())) {
			if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.POTION))) {
				mHavePotion = true;
				mPrePotion = su.getConfig().getBool(Property.SIMULATION_PREPOTION);
			} else if (!effect.triggers.contains(ProcEffect.Trigger.USER) && effect.rate == 0 && effect.ppm == 0) {
				mSimEngine.getView().onWarn("Proc effect \"" + effect.name + "\" has no rate information.");
				continue;
			}

			mSimEngine.addProc(new SimulationProc(mSimEngine, mModel, effect));
		}

		// Register the actions
		mSimEngine.addAction(Action.AUTOATTACK_STOP, new SimulationAction() {
			@Override
			public double perform() {
				mMeleeEv.stop();
				return 0.0;
			}
		}, false);
		mSimEngine.addAction(Action.AUTOATTACK_START, new SimulationAction() {
			@Override
			public double perform() {
				mMeleeEv.start();
				return mMeleeEv.isUp() ? 0.0 : ACTION_FAIL;
			}
		}, false);
		mSimEngine.addAction(Action.BATTLESHOUT, mBattleShoutAction);
		mSimEngine.addAction(Action.BERSERKERRAGE, mBerserkerRageAction, false);
		mSimEngine.addAction(Action.CHARGE, mChargeAction, false);
		mSimEngine.addAction(Action.CLEAVE, mCleaveAction, false);
		mSimEngine.addAction(Action.COMMANDINGSHOUT, mCommandingShoutAction);
		mSimEngine.addAction(Action.CONCUSSIONBLOW, mConcussionBlowAction);
		mSimEngine.addAction(Action.DEMORALIZINGSHOUT, mDemoralizingShoutAction);
		mSimEngine.addAction(Action.DEVASTATE, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mDevastateAction.perform();
			}
		});
		mSimEngine.addAction(Action.ENRAGEDREGENERATION, mEnragedRegenerationAction);
		mSimEngine.addAction(Action.INNERRAGE, mInnerRageAction, false);
		mSimEngine.addAction(Action.HEROICSTRIKE, mHeroicStrikeAction, false);
		mSimEngine.addAction(Action.HEROICTHROW, mHeroicThrowAction, true);
		mSimEngine.addAction(Action.HEROISM, mHeroismEv, false);
		mSimEngine.addAction(Action.LASTSTAND, mLastStandAction, false);
		mSimEngine.addAction(Action.NONE, new SimulationAction() {
			@Override
			public double perform() {
				return ACTION_FAIL;
			}
		});
		mSimEngine.addAction(Action.POTION, mPotionAction, false);
		mSimEngine.addAction(Action.PUMMEL, mPummelAction, false);
		mSimEngine.addAction(Action.RECKLESSNESS, mRecklessnessAction, false);
		mSimEngine.addAction(Action.REND, mRendAction, false);
		mSimEngine.addAction(Action.REVENGE, mRevengeAction);
		mSimEngine.addAction(Action.RETALIATION, mRetaliationAction);
		mSimEngine.addAction(Action.SHATTERINGTHROW, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				if (mShatteringThrowCooldownExpireTime > mSimEngine.getSimTime())
					return ACTION_FAIL;

				mShatteringThrowCooldownExpireTime = mSimEngine.getSimTime() + mModel.shatteringThrowCDDuration;

				return mActiveTargetData.mShatteringThrowEv.perform();
			}
		}, false);
		mSimEngine.addAction(Action.SHIELDBLOCK, mShieldBlockAction, false);
		mSimEngine.addAction(Action.SHIELDSLAM, mShieldSlamAction);
		mSimEngine.addAction(Action.SHIELDWALL, mShieldWallAction, false);
		mSimEngine.addAction(Action.SHOCKWAVE, mShockwaveAction);
		mSimEngine.addAction(Action.THUNDERCLAP, mThunderClapAction);
		mSimEngine.addAction(Action.UNHOLYFRENZY, mUnholyFrenzyEv, false);
		mSimEngine.addAction(Action.VICTORYRUSH, mVictoryRushAction, false);
	}

	// Damage multipliers
	final double getDirectDamageMult() {
		double mult = 1;

		mult *= (1 - mActiveTargetData.getArmorDR(mModel.mToon.level));
		mult *= mModel.multPhysVuln;
		mult *= mModel.multDamageMult;
		mult *= mBastionOfDefenseEv.isUp() ? mModel.multBastionOfDefense : 1;
		mult *= mRudeInterruptionEv.isUp() ? mModel.multRudeInterruption : 1;
		mult *= mTier_12_Dps_2pcEv.isUp() ? mModel.tier_12_dps_2pc_damageMult : 1;
		mult *= mToTTEv.getMult();

		return mult;
	}

	final double getBleedDamageMult() {
		double mult = 1;

		// ToTT, Bastion of Defense (Enrage) and Rude Interruption are handled elsewhere.
		mult *= mModel.multBleedDarkIntent;
		mult *= mModel.multBleedMult;
		mult *= mModel.multPhysVuln;
		mult *= mModel.multDamageMult;

		return mult;
	}

	final double getSpellDamageMult() {
		double mult = 1;

		mult *= mModel.multSpellVuln;
		mult *= mModel.multDamageMult;
		mult *= mToTTEv.getMult();
		mult *= mRudeInterruptionEv.isUp() ? mModel.multRudeInterruption : 1;

		return mult;
	}

	// Resource management
	final boolean hasRage(int rage) {
		return hasRage(rage, true);	// Most things are discounted by Battle Trance.
	}

	final boolean hasRage(int rage, boolean useBattleTrance) {
		if (mUnlimitedRage == true) return true;

		if (useBattleTrance && rage > 5 && mBattleTranceEv.isUp())
			return true;

		return mRage >= rage;
	}

	final double onRageGain(double rage) {
		double before = mRage;

		mRage = mRage + rage;
		if (mRage > 100) {
			mRageOverflow += (mRage - 100);
			mRage = 100;
		}

		return (mUnlimitedRage) ? rage : mRage - before;
	}

	final void onRageGainThreat(double rage) {
		mRageGainedCountThreat += Math.floor(onRageGain(rage));
	}

	final void onRageLoss(int rage) {
		onRageLoss(rage, true);
	}

	final void onRageLoss(int rage, boolean consumeBattleTrance) {
		if (rage > 5 && consumeBattleTrance && mBattleTranceEv.isUp()) {
			mBattleTranceEv.cancel();
			mBattleTranceEv.mBattleTranceRageReturn += rage;
		} else {
			if (!mUnlimitedRage) mRage = Math.max(0, mRage - rage);

			mRageSpent += rage;
		}
	}

	// Status query methods.
	public final double getEncounterDuration() {
		return mSimEngine.getEndTime();
	}

	public final double getTimeRemaining() {
		return mSimEngine.getEndTime() - mSimEngine.getSimTime();
	}

	public final double getElapsedTime() {
		return mSimEngine.getSimTime();
	}

	public final int getLevel() {
		return mSimEngine.getToon().level;
	}

	public final boolean isInCombat() {
		return mSimEngine.getSimTime() > 0.0;
	}

	public final boolean isInGCD() {
		return mSimEngine.isInGCD();
	}

	public final boolean isAutoAttackEnabled() {
		return mMeleeEv.isUp();
	}

	public final int getRage() {
		return (int) Math.floor(mRage);
	}

	public final boolean hasUnlimitedRage() {
		return mUnlimitedRage;
	}

	public final boolean hasPotion() {
		return mHavePotion;
	}

	public final boolean canPotion() {
		// Don't potion out of combat if the user does not want pre-potion.
		if (!mPrePotion && !isInCombat()) return false;

		return mPotionAction.canPotion();
	}

	public final boolean shouldPummel() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().isUp();
	}

	public final boolean isTargetCasting() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().isCasting();
	}

	public final double nextCastIn() {
		if (mActiveTargetData == null) return 0.0;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().nextCastIn();
	}

	public final double getTargetCastTime() {
		if (mActiveTargetData == null) return 0.0;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().getTargetCastTime();
	}

	// Abilities
	public final double getChargeCD() {
		return mChargeAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mChargeAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean canCharge() {
		if (isInCombat()) return false;	// TODO: Eventually support Warbringer

		// Only allow this if we have points in Stampede.
		return getChargeCD() == 0.0;
	}

	public final int getPummelCost() {
		return mModel.pummelCost;
	}

	public final double getPummelCD() {
		return mPummelAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mPummelAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getShieldBlockCost() {
		return mModel.shieldBlockCost;
	}

	public final boolean isShieldBlockUp() {
		return mShieldBlockAction.isShieldBlockUp();
	}

	public final double getShieldBlockCD() {
		return mShieldBlockAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mShieldBlockAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getShieldBlockRemaining() {
		return mShieldBlockAction.isUp() ? mShieldBlockAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getDemoralizingShoutCost() {
		return mModel.demoralizingShoutCost;
	}

	public final boolean isDemoralizingShoutUp() {
		return mDemoralizingShoutAction.isUp();
	}

	public final double getDemoralizingRoarRemaining() {
		return mDemoralizingShoutAction.isUp() ? mDemoralizingShoutAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getBattleShoutCD() {
		return mShoutCooldownExpireTime > mSimEngine.getSimTime() ? mShoutCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getCommandingShoutCD() {
		return mShoutCooldownExpireTime > mSimEngine.getSimTime() ? mShoutCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getBerserkerRageCD() {
		return mBerserkerRageAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mBerserkerRageAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isInnerRageUp() {
		return mInnerRageAction.isUp();
	}

	public final double getInnerRageCD() {
		return mInnerRageAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mInnerRageAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getInnerRageRemaining() {
		return mInnerRageAction.isUp() ? mInnerRageAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getShieldSlamCD() {
		return mShieldSlamAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mShieldSlamAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getShieldSlamCost() {
		if (isSwordAndBoardUp() || mBattleTranceEv.isUp()) return 0;

		return mModel.shieldSlamCost;
	}

	public final boolean isSwordAndBoardUp() {
		return mSwordAndBoardEv.isUp();
	}

	public final double getSwordAndBoardProcRemaining() {
		return mSwordAndBoardEv.isUp() ? mSwordAndBoardEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isSunderArmorUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mDevastateAction.mStacks > 0;
	}

	public final int getSunderArmorStacks() {
		if (mActiveTargetData == null) return 0;
		return mActiveTargetData.mDevastateAction.mStacks;
	}

	public final double getSunderArmorRemaining() {
		return isSunderArmorUp() ? mActiveTargetData.mDevastateAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final int getDevastateCost() {
		if (mActiveTargetData == null) return Integer.MAX_VALUE;
		if (mActiveTargetData.mDevastateAction.mHasDevastate == false) return Integer.MAX_VALUE;

		if (mBattleTranceEv.isUp()) return 0;

		return mModel.devastateCost;
	}

	public final int getRevengeCost() {
		return mModel.revengeCost;
	}

	public final boolean canRevenge() {
		return mRevengeAction.isUp() && mRevengeAction.mCooldownExpireTime <= mSimEngine.getSimTime();
	}

	public final boolean hasThunderstruck() {
		return mThunderstruckEv.mIsEnabled;
	}

	public final boolean isThunderstruckUp() {
		return mThunderstruckEv.isUp();
	}

	public final int getThunderstruckStacks() {
		return mThunderstruckEv.mStacks;
	}

	public final double getThunderstruckRemaining() {
		return isThunderstruckUp() ? mThunderstruckEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isThunderClapUp() {
		return mThunderClapAction.isUp();
	}

	public final int getThunderClapCost() {
		if (mBattleTranceEv.isUp()) return 0;

		return mModel.thunderClapCost;
	}

	public final double getThunderClapRemaining() {
		return isThunderClapUp() ? mThunderClapAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getThunderClapCD() {
		return mThunderClapAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mThunderClapAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean hasBloodAndThunder() {
		return mThunderClapAction.mBloodAndThunderEnabled;
	}

	public final boolean isRendUp() {
		return mRendAction.isUp();
	}

	public final int getRendCost() {
		if (mBattleTranceEv.isUp()) return 0;

		return mModel.rendCost;
	}

	public final double getRendRemaining() {
		return isRendUp() ? mRendAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getConcussionBlowCD() {
		return mConcussionBlowAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mConcussionBlowAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getConcussionBlowCost() {
		if (mBattleTranceEv.isUp()) return 0;

		return mModel.concussionBlowCost;
	}

	public final double getShockwaveCD() {
		return mShockwaveAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mShockwaveAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getShockwaveCost() {
		if (mBattleTranceEv.isUp()) return 0;

		return mModel.shockwaveCost;
	}

	public final double getHeroicThrowCD() {
		return mHeroicThrowAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mHeroicThrowAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getHeroicStrikeCD() {
		return mHeroicStrikeCooldownExpireTime > mSimEngine.getSimTime() ? mHeroicStrikeCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean hasGagOrder() {
		return mHeroicThrowAction.mHasGagOrder;
	}

	public final double getHeroicStrikeCost() {
		if (mBattleTranceEv.isUp()) return 0;
		if (mModel.mTier_13_dps_2pc && mInnerRageAction.isUp()) return mModel.heroicStrikeCost - 10;

		return mModel.heroicStrikeCost;
	}

	public final double getCleaveCD() {
		return mHeroicStrikeCooldownExpireTime > mSimEngine.getSimTime() ? mHeroicStrikeCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getCleaveCost() {
		if (mBattleTranceEv.isUp()) return 0;

		return mModel.cleaveCost;
	}

	public final boolean isShieldWallUp() {
		return mShieldWallAction.isUp();
	}

	public final double getShieldWallCD() {
		double cd =  mShieldWallAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mShieldWallAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
		if (mRecklessnessAction.isUp()) return Math.max(cd, getRecklessnessRemaining());
		if (mRetaliationAction.isUp()) return Math.max(cd, getRetaliationRemaining());

		return cd;
	}

	public final double getShieldWallRemaining() {
		return mShieldWallAction.isUp() ? mShieldWallAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isRecklessnessUp() {
		return mRecklessnessAction.isUp();
	}

	public final double getRecklessnessCD() {
		double cd = mRecklessnessAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mRecklessnessAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
		if (mRetaliationAction.isUp()) return Math.max(cd, getRetaliationRemaining());
		if (mShieldWallAction.isUp()) return Math.max(cd,  getShieldWallRemaining());

		return cd;
	}

	public final double getRecklessnessRemaining() {
		return mRecklessnessAction.isUp() ? mRecklessnessAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isRetaliationUp() {
		return mRetaliationAction.isUp();
	}

	public final double getRetaliationCD() {
		double cd =  mRetaliationAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mRetaliationAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
		if (mRecklessnessAction.isUp()) return Math.max(cd, getRecklessnessRemaining());
		if (mShieldWallAction.isUp()) return Math.max(cd,  getShieldWallRemaining());

		return cd;
	}

	public final double getRetaliationRemaining() {
		return mRetaliationAction.isUp() ? mRetaliationAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isLastStandUp() {
		return mLastStandAction.isUp();
	}

	public final double getLastStandCD() {
		return mLastStandAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mLastStandAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getLastStandRemaining() {
		return mLastStandAction.isUp() ? mLastStandAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isEnragedRegenerationUp() {
		return mEnragedRegenerationAction.isUp();
	}

	public final double getEnragedRegenerationCD() {
		return mEnragedRegenerationAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mEnragedRegenerationAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getEnragedRegenerationRemaining() {
		return mEnragedRegenerationAction.isUp() ? mEnragedRegenerationAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isEnraged() {
		return mBerserkerRageAction.isUp() || mBastionOfDefenseEv.isUp() || mUnholyFrenzyEv.isUp();
	}

	public final boolean hasImpendingVictory() {
		return mImpendingVictoryEv.mIsEnabled;
	}

	public final boolean isImpendingVictoryUp() {
		return mImpendingVictoryEv.isUp();
	}

	// Externals
	public final boolean isHeroismUp() {
		return mHeroismEv.isUp();
	}

	public final double getHeroismCD() {
		return mHeroismEv.mCooldownExpireTime > mSimEngine.getSimTime() ? mHeroismEv.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getHeroismRemaining() {
		return mHeroismEv.isUp() ? mHeroismEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isUnholyFrenzyUp() {
		return mUnholyFrenzyEv.isUp();
	}

	public final double getUnholyFrenzyCD() {
		return mUnholyFrenzyEv.mCooldownExpireTime > mSimEngine.getSimTime() ? mUnholyFrenzyEv.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getUnholyFrenzyRemaining() {
		return mUnholyFrenzyEv.isUp() ? mUnholyFrenzyEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isShatteringThrowUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mShatteringThrowEv.isUp();
	}

	public final double getShatteringThrowCD() {
		return mShatteringThrowCooldownExpireTime > mSimEngine.getSimTime() ? mShatteringThrowCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getShatteringThrowRemaining() {
		return isShatteringThrowUp() ? mActiveTargetData.mShatteringThrowEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	// Procs
	public final Map<String, SimulationProcStatus> getProcStatusMap() {
		return mSimEngine.getProcStatusMap();
	}

	public final boolean hasTier_13_2pc() {
		return mModel.mToon.tier_13_2pc;
	}
}
